#!/bin/sh
# Burn audio files to a blank CD.

# Copyright 2016-2019 orbea
# All rights reserved.
#
# Redistribution and use of this script, with or without modification, is
# permitted provided that the following conditions are met:
#
# 1. Redistributions of this script must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
#  THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
#  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
#  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO
#  EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
#  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
#  OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
#  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
#  OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
#  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# This is required for the legacy ash in Slackware and can be removed
# once ash is no longer included in any supported Slackware releases.
# shellcheck disable=SC2004
# https://github.com/koalaman/shellcheck/wiki/SC2004

IFS=' 	
'
\unset -f command printf read unalias : 2>/dev/null
\unalias -a 2>/dev/null
PATH="$(command -p getconf PATH):$PATH"
LC_ALL=C; export LC_ALL

set -euf

au=; audio=; cmd=; farr=; final=; flac=; format=; fsize=; ignore=; list=; narr=
new=; ord=; out=; req=; sec=; tmp=; warr=; wav=

DEBUG=; DRYRUN=; VERBOSE=

PRGNAM=burncd

DEVICE='/dev/sr1'
OUTPUT='/tmp/CD'

die () {
  ret="$1"; shift
  case "$ret" in
    : ) printf %s\\n "$@" >&2; return 0 ;;
    0 ) printf %s\\n "$@" ;;
    * ) printf %s\\n "$@" >&2 ;;
  esac
  exit "$ret"
}

escape () {
  word="$1"; base=
  case "$word" in
    *\\*|*\$*|*\"*|*\`* )
      while [ "$word" != '' ]; do
        first="$(printf %s "${word%"${word#?}"}")"
        word="$(printf %s "${word#?}")"
        case "$first" in
          \\|\$|\"|\` ) first="\\$first" ;;
          * ) : ;;
        esac
        base="${base}$first"
      done
    ;;
    * )
      base="$word"
    ;;
  esac
}

exists () {
  v=1
  while [ $# -gt 0 ]; do
    arg="$1"; shift
    case "$arg" in ''|*/) continue ;; esac
    x="${arg##*/}" z="${arg%/*}"
    [ ! -f "$z/$x" ] || [ ! -x "$z/$x" ] && [ "$z/$x" = "$arg" ] && continue
    [ "$x" = "$z" ] && [ -x "$z/$x" ] && [ ! -f "$arg" ] && z=
    p=":$z:$PATH"
    while [ "$p" != "${p#*:}" ]; do
      p="${p#*:}"; d="${p%%:*}"
      if [ -f "$d/$x" ] && [ -x "$d/$x" ]; then
        printf %s\\n "$d/$x"
        v=0
        break
      fi
    done
  done
  return $v
}

mklist () {
  ch=; ll=
  action="$1"; shift
  { empty "$action" + && { ml="$1"; eval "ch=\"\$$ml\""; shift; }; } || :
  while [ $# -gt 0 ]; do
    mk="$1"; shift
    case "$mk" in -*) break ;; esac
    n="$(($n+1))"
    empty "$action" + || { ll="$mk"; break; } || :
    case "$ll" in *$mk*) continue ;; esac
    case "$ch" in *$mk*) continue ;; esac
    case "$mk" in
      au|flac|wav ) ll="${ll} $mk" ;;
      * ) die 1 "Unrecognized audio format '$mk', use -h for help." ;;
    esac
  done
  { empty "$ll" && return 0; } || :
  case "$action" in
    + ) eval "$ml=\"\$${ml#${ml%%[! ]*}} ${ll#${ll%%[! ]*}}\"" ;;
    * ) eval "$action=${ll:-\$$action}" ;;
  esac
}

CDRECORD="${CDRECORD:-$(exists cdrecord || :)}"
FLAC="${FLAC:-$(exists flac || :)}"
SOX="${SOX:-$(exists sox || :)}"

FILE="$(exists file || :)"

dry () { case "${DRYRUN:-0}" in 1) : ;; *) "$@" ;; esac; }
empty () { case "${1:-}" in "${2:-}") return 0 ;; *) return 1 ;; esac; }
exclude () { case "$ignore" in *$1*) fmt= ;; *) fmt="$2"; suf="$3" ;; esac; }
log () { case "${VERBOSE:-0}" in 1) printf %s\\n "$@" ;; *) : ;; esac; }
printn () { eval "set -- $1"; printf %s\\n $#; }
quiet () { case "${DEBUG:-0}" in 1) "$@" ;; *) "$@" 2>/dev/null ;; esac; }
quit () { err=$?; eval "set -- $out $tmp"; command -p rm -f -- "$@"; }
use () { :; }

usage="$PRGNAM - Burn audio files from the current directory to a blank CD.

  Usage: $PRGNAM [OPTIONS]
    -d, --debug,    Debug output from external programs.
    -e, --exclude,  Exclude audio formats.
    -f, --format,   Choose the audio format.
    -h, --help,     Show this help message.
    -n, --dry-run,  Enable a test run without burning to a CD.
    -o, --output,   Path of the temporary directory.
    -v, --verbose,  Verbose logging.
    -V. --version,  Show the $PRGNAM version number.
    -z, --device,   Path of the CD drive.

  Supported audio formats:
    au, flac, wav

  Environment variables:
    CDRECORD : Path of the cdrecord binary. ($CDRECORD)
    FLAC : Path of the flac binary. ($FLAC)
    SOX : Path of the sox binary. ($SOX)

  To use cdrecord as a normal user:
    # chown root:somegroup $CDRECORD
    # chmod 4710 $CDRECORD"

for flag do
  case "$flag" in
    -- ) break ;;
    -d|--debug ) : ;;
    -e|--exclude ) : ;;
    -f|--format ) : ;;
    -h|--help ) die 0 "$usage" ;;
    -n|--dry-run ) : ;;
    -o|--output ) : ;;
    -v|--verbose ) : ;;
    -V|--version ) die 0 "$PRGNAM 0.0" ;;
    -z|--device ) : ;;
    * ) die 1 "Unrecognized option '$flag', use -h for help." ;;
  esac
done

while [ $# -gt 0 ]; do
  n=0; option="$1"; shift
  case "$option" in
    -- ) break ;;
    -d|--debug ) DEBUG=1 ;;
    -e|--exclude ) empty "${1+x}" || mklist + ignore "$@" ;;
    -f|--format ) empty "${1+x}" || mklist + format "$@" ;;
    -n|--dry-run ) DRYRUN=1 ;;
    -o|--output ) empty "${1+x}" || mklist OUTPUT "$1" ;;
    -v|--verbose ) VERBOSE=1 ;;
    -z|--device ) empty "${1+x}" || mklist DEVICE "$1" ;;
  esac
  shift "$n"
done

if ! empty "$(command -p ls)"; then
  set +f
  set -- ./*
  set -f
else
  die 1 'ERROR: Directory is empty.'
fi

log '' 'Configured options:' "	DEVICE = $DEVICE" "	OUTPUT = $OUTPUT" \
  "	Preferred audio formats = ${format# }" \
  "	Excluded audio foramts = ${ignore# }" ''

for l in wav au flac; do
  case "$format" in
    *$l* ) : ;;
    * ) format="${format} $l" ;;
  esac
done

for l in $(printf %s "$format"); do
  case "$ignore" in
    *$l* ) : ;;
    * ) final="${final} $l" ;;
  esac
done

n=0

for f do
  [ -d "$f" ] || [ -r "$f" ] ||
    { die : 'WARNING: File can not be read.' "Skipping '$f'."; continue; }
  file="$(command -p "$FILE" "$f")"
  type="$(printf %s "${file#"$f: "}")"
  case "$type" in
    *FLAC* ) exclude flac farr wav ;;
    *Sun/NeXT* ) exclude au narr au ;;
    *WAVE* ) exclude wav warr wav ;;
    * ) fmt= ;;
  esac
  if [ "${fmt}" ]; then
    head="${f##*/}"
    name="${head%.*}.$suf"
    if [ -e "$OUTPUT/$name" ] || [ -e "$OUTPUT/tmp-$name" ]; then
      die : "WARNING: File found in $OUTPUT." "Skipping '$f'."
      continue
    fi
    escape "$head"
    cmn="$type::${base}:$n:$fmt:$suf"
    case $fmt in
      farr ) flac="$flac \"${cmn}:\\\$FLAC:16 bit:44.1 kHz:stereo\"" ;;
      narr ) au="$au \"${cmn}:null:16-bit:44100 Hz:stereo\"" ;;
      warr ) wav="$wav \"${cmn}:null:16 bit:44100 Hz:stereo\"" ;;
    esac
    n=$(($n+1))
  fi
done

for f in $(printf %s "$final"); do
  eval "audio=\"\${audio} \${$f}\""
done

eval "set -- $audio"
for f do
  song="${f#*::}"
  for i in cnl khz bit ext end lst pos; do
    eval "$i=\"\${song##*:}\""
    song="${song%:*}"
  done
  add=1
  eval "set -- $new"
  while [ $# -gt 0 ]; do
    empty "${1%.*}" "${song%.*}" && { add=0; break; }
    shift
  done
  empty "$add" 1 || continue
  type="${f%%::*}"; var=
  escape "$song"
  name="${base%.*}.${end:?}"
  new="$new \"$base\""
  ord="$ord \"$OUTPUT/$name:${pos:?}\""
  out="$out \"$OUTPUT/$name\""
  tmp="$tmp \"$OUTPUT/tmp-$name\""
  eval "${lst:?}=\"\${$lst} \\\"\$base\"\\\""
  if ! empty "${ext:?}" null; then
    case "$req" in
      *$ext* ) : ;;
      * ) req="${req} $ext" ;;
    esac
  fi
  for s in "${bit:?}" "${khz:?}" "${cnl:?}"; do
    case "$type" in *$s*) : ;; *)
      case "$s" in
        "$bit" ) var="${var} -b 16" ;;
        "$khz" ) var="${var} -r 44.1k" ;;
        "$cnl" ) var="${var} -c 2" ;;
      esac ;;
    esac
  done
  empty "$var" || cmd="$cmd \"$name:${var#"${var%%[! ]*}"}\""
done

empty "$new" && die 1 "ERROR: No music files found in $PWD."

for prgnam in \$CDRECORD $(printf %s "$req"); do
  dep="$(eval printf %s "$prgnam")"
  exists "$dep" >/dev/null 2>&1 && { log "$(exists "$dep"): ok"; continue; }
  if empty "$dep"; then
    case "$prgnam" in
      \$CDRECORD ) dep=cdrecord ;;
      \$FLAC ) dep=flac ;;
    esac
  fi
  die 1 "ERROR: $dep is not in $PATH."
done

if exists "$SOX" >/dev/null 2>&1; then
  eval "set -- $new"
  for t in $(quiet "$SOX" --i -D -- "$@"); do
    sec="$(($sec+$(printf '%.0f' "$t")))"
  done
  dur="$(printf '%dh %dm %ds' $(($sec/3600)) $(($sec%3600/60)) $(($sec%60)))"
  log "$(exists "$SOX"): ok" "Total duration: $dur"
  if [ "$sec" -gt 4800 ]; then
    die 1 "ERROR: Total duration of $dur exceeds 80 minutes."
  fi
elif ! empty "$cmd"; then
  die 1 'ERROR: sox is required for converting to 16 bit / 44.1 kHz / stereo.'
else
  die : 'WARNING: sox is required for checking the duration of the audio files.'
fi

while :; do
  if [ ! -e "$DEVICE" ]; then
    die : "WARNING: $DEVICE not found." 'Configure the device. (i.e. /dev/sr0)'
    read -r DEVICE
    continue
  fi
  if empty $DRYRUN; then
    blank="$(quiet "$CDRECORD" dev="$DEVICE" -minfo)" ||
      die 1 "$CDRECORD: operation failed."
    case "$blank" in *Blank*) : ;; *)
      die : "WARNING: Blank CD not found in $DEVICE." \
        'Insert a blank CD. Continue? (y/n)'
      read -r answer
      case "$answer" in
        [yY]|[yY][eE][sS] ) continue ;;
        * ) die 0 "Blank CD not found in $DEVICE. Aborting ..." ;;
      esac ;;
    esac
  fi
  log "Using blank CD found in $DEVICE."
  break
done

farth=; fburn=; fmsg=; narth=; nburn=; nmsg=; warth=; wburn=; wmsg=

for b in "$farr" "$narr" "$warr"; do
  case "$b" in
    '' )
      :
    ;;
    "$farr" )
      farth='21/10'
      fburn="quiet \"\$FLAC\" --output-prefix=\"\$OUTPUT\"/ --decode -- \"\$@\""
      fmsg='Decoding flac files ...'
      list="$list \"'\\\$farr' '\\\$farth' '\\\$fburn' '\\\$fmsg'\""
    ;;
    "$narr" )
      narth='19/13'
      nburn="quiet die : \"\$@\"; for a do command -p cp -- \
             \"\$a\" \"\$OUTPUT/\${a%.*}.au\"; done"
      nmsg='Copying Sun/NeXT audio files ...'
      list="$list \"'\\\$narr' '\\\$narth' '\\\$nburn' '\\\$nmsg'\""
    ;;
    "$warr" )
      warth='19/13'
      wburn="quiet die : \"\$@\"; for a do command -p cp -- \
             \"\$a\" \"\$OUTPUT/\${a%.*}.wav\"; done"
      wmsg='Copying wav files ...'
      list="$list \"'\\\$warr' '\\\$warth' '\\\$wburn' '\\\$wmsg'\""
    ;;
  esac
done

use "$farth" "$fburn" "$fmsg" "$narth" "$nburn" "$nmsg" "$warth" "$wburn" \
  "$wmsg"

command -p mkdir -p -- "$OUTPUT"

eval "set -- $list"
for i do
  eval "set -- $i"
  eval "math=$2"
  eval "eval \"set -- $1\""
  fline="$(command -p wc -c -- "$@" | command -p tail -n1)"
  asize="$(((${fline%%[ ]*}+512)/1024*${math:?}))"
  fsize="$(($fsize+$asize))"
done

tline="$(command -p df -P -- "$OUTPUT" |
       { read -r _; read -r l; printf %s "$l"; })"
eval "set -- $tline"
tsize="$(eval "printf %s \${4}")"
log "Estimated size of audio files: $fsize KB" \
  "Free space in $OUTPUT: $tsize KB"

if [ "$fsize" -ge "$tsize" ]; then
  c=0
  for size in "$fsize" "$tsize"; do
    eval "mb$c=\"$((($size+512)/1024)) MB\""
    c="$(($c+1))"
  done
  die 1 "ERROR: ${mb0:?} required in $OUTPUT to decode flac files." \
    "Disk space: ${mb1:?}"
fi

if [ "$(printn "$list")" -gt 1 ]; then
  out=; n=0
  arg=$(printn "$ord")
  while [ "$(printn "$out")" -lt "$arg" ]; do
    eval "set -- $ord"
    for i do
      if empty "${i##*:}" $n; then
        escape "${i%:*}"
        out="$out \"$base\""
        break
      fi
    done
    n=$(($n+1))
  done
fi

trap 'quit; trap - EXIT; exit $err' EXIT INT

eval "set -- $list"
for i do
  eval "set -- $i"
  eval "burn=$3"
  eval "printf '%s\\n' \"$4\""
  eval "eval \"set -- $1\""
  eval "${burn:?}"
done

eval "set -- $cmd"
for i do
  file="${i%:*}"
  opt="${i##*:}"
  log "Converting $file" "sox options: $opt"
  eval "set -- $opt"
  quiet "$SOX" -- "$OUTPUT/$file" "$@" -- "$OUTPUT/tmp-$file"
  command -p rm -f -- "$OUTPUT/$file"
  command -p mv -- "$OUTPUT/tmp-$file" "$OUTPUT/$file"
done

eval "set -- $out"
log 'Burning audio files:' "$@"
dry quiet "$CDRECORD" dev="$DEVICE" -dao -audio -pad -- "$@"
die 0 'CD burning finished successfully.'
